Skip to content

fix(metadata-protocol): deleting an own-package object extension fills the stored computed fields it hands back - #33350

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/area-E1-32958
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/area-E1-32958

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread
Part of #32958
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none — the published skills teach neither the delete door's answer keys nor the stored-expression fill any metadata door runs; an optional key on the delete answer changes nothing an author writes.

维护者速读

  • 改了什么:删除自有包的 object_extension(它曾重定义一个存储型计算字段的表达式)时,删除入口现在会像保存、回滚、撤销入口一样,把对象已有记录按恢复后的表达式重算回填,并在返回里用新的可选 advisories 逐字段说明。
  • 为什么改:删除扩展后对象已改用原表达式计算,但老记录仍是被删表达式算出的值,报表、筛选、汇总读到的都是旧数,且没有任何提示。
  • 风险与回滚:回填只在删除确实让某个存储计算字段改变时才跑,复用保存入口同一条回填路径;回填失败不会让删除失败,只在 advisories 里报未完成。回滚即 revert 本 PR。
  • 先看:packages/metadata-protocol/src/protocol-diff-delete-load.ts(注册表 heal 前后的 capture/fill 两处)、packages/spec/src/api/protocol.zod.ts(DeleteMetaItemResponseSchema.advisories)、packages/objectql/src/stored-expression-backfill.test.ts(两种保存顺序的钉子)。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/stored-expression-backfill.test.ts -t 32958,预期 4 个用例全绿(本修复之前前两个是红的:行值停在 [8, 16, 24])。
  • 需要你做的:无,席位落地。

What changes

deleteMetaItem (DELETE /api/v1/meta/:type/:name) removed the row and healed the registry, so the object served the definition beneath the removed one, but it ran no stored-expression fill. Deleting an own-package object_extension that re-declared a stored computed field therefore left every existing row on the value the removed expression computed.

Tests

  • packages/objectql/src/stored-expression-backfill.test.ts, new top-level describe('[#32958] …') in the existing door harness (real protocol, real engine):
    1. object saved, then the extension: delete ⇒ registry serves the owner's expression, rows [1, 2, 3], 3 row writes, one stored-expression-filled notice, and the answer parses through DeleteMetaItemResponseSchema unstripped. CONTROL before the delete: the extension's expression served and rows [8, 16, 24].
    2. extension saved, then the object: same assertions, same CONTROL.
    3. an extension that changed no stored computed field: no row write, no advisories key.
    4. a draft discard: no row write, no advisories key.
  • packages/spec/src/api/protocol.test.ts: the pin that said the delete answer declares neither version nor advisories is split: version stays undeclared; advisories is declared, carried, and absent on a clean answer.

Acceptance notes

  • The card's second item is NOT in this PR, hence Part of #32958: the object-storage-sync-refused text (objectStorageSyncRefusedNotice) still speaks of "this save" and "publish the object again" on rollback and revert answers. That function lives in packages/metadata-protocol/src/protocol-registry-hydration.ts, a file held by in-flight E1 work for this pilot, so it was not edited. The delete door added here runs no storage sync and never emits that notice.
  • The no-row branch of the delete door (no stored row, a stale registry shadow healed) captures and fills nothing; it is unchanged.
  • The legacy raw-engine delete path serves only code-only types (job, agent) and defines no object; unchanged.
  • A removed extension that changed a stored field's TYPE (not only its expression) gets the fill but no storage sync, as the revert door's removal limb does today.
  • Overlapping open PR checked: fix(metadata-protocol)!: history and rollback doors refuse a bare two-holder name and address one holder by ?package= #33193 edits packages/spec/src/api/protocol.zod.ts in other schemas (history/rollback request), not DeleteMetaItemResponseSchema.

Verification

Premise on origin/main (e540baf), both orders: red
× object saved first, then the extension: the delete rewrites every row the removed expression computed
× extension saved first, then the object: the delete rewrites every row the removed expression computed
AssertionError: expected [ 8, 16, 24 ] to deeply equal [ 1, 2, 3 ]   (both, at the rows assertion; the registry assertion before it passed)
Tests  2 failed | 2 passed | 60 skipped (64)
Ablation of the omitted-when-empty answer (one mutation, both negative pins)

Mutation via scripts/ablation-replace.mjs: the spread of advisories replaced by an unconditional advisories key; ablation-dist-preflight proved the marker in metadata-protocol/dist (2 files). Result: tests 3 and 4 red (Tests 2 failed | 2 passed). Restore leg: git checkout HEAD, blob hash equal to HEAD's, rebuild, --absent preflight clean, whole tree clean; all 4 green again.

Local runs on the merged head
  • pnpm --filter @objectstack/metadata-protocol test: Test Files 339 passed, 9 skipped; Tests 22843 passed (lock VERDICT command-exit 0).
  • pnpm --filter @objectstack/objectql test: Test Files 595 passed; Tests 12946 passed (lock VERDICT command-exit 0).
  • @objectstack/spec, narrowed to the 36 test files that import the protocol schema or the authorable surface: Tests 2340 passed. The full spec suite was not run locally: the shared verify lock was held by another session's workspace-wide test run for over 30 minutes. CI's Test Core runs it.
  • pnpm typecheck for spec, metadata-protocol and objectql: exit 0.
  • pnpm lint: exit 0.
  • pnpm --filter @objectstack/spec check:generated: all 14 artifacts up to date.
  • The 119 derived gate families (dispatch-gates --commands): 116 exit 0; 3 exit 3 (check:skill-examples, check:dual-build-cjs-loads, check:type-check-debt: PREREQUISITE NOT MET, unbuilt packages outside the closure), NOT MEASURED locally, CI runs them.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cy1y7UNVbT2QAdmgdn

…wn-package object extension (red on main)

Claude-Session: https://claude.ai/code/session_016WV4cy1y7UNVbT2QAdmgdn
Co-authored-by: Claude <noreply@anthropic.com>
…lds a removed object extension hands back

Claude-Session: https://claude.ai/code/session_016WV4cy1y7UNVbT2QAdmgdn
Co-authored-by: Claude <noreply@anthropic.com>
… the delete answer's advisories

Claude-Session: https://claude.ai/code/session_016WV4cy1y7UNVbT2QAdmgdn
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/spec, touching 11 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

29 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 82838e83adc6f464d2baef055ff7eed6338243f9.

⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 173 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 82838e83adc6f464d2baef055ff7eed6338243f9 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b51ec5777a46be8f74a4df48c1b7eaf8268e8ea8 — the merge of head d7dd7d7b1333eae9a128ba36a960735ebb9f595e into base 82838e83adc6f464d2baef055ff7eed6338243f9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b51ec5777a46be8f74a4df48c1b7eaf8268e8ea8 && git checkout b51ec5777a46be8f74a4df48c1b7eaf8268e8ea8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 82838e83adc6f464d2baef055ff7eed6338243f9 d7dd7d7b1333eae9a128ba36a960735ebb9f595e && git checkout -B drift-repro 82838e83adc6f464d2baef055ff7eed6338243f9 && git merge --no-ff d7dd7d7b1333eae9a128ba36a960735ebb9f595e

node scripts/docs-audit/affected-docs.mjs --json 82838e83adc6f464d2baef055ff7eed6338243f9

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 82838e83adc6f464d2baef055ff7eed6338243f9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4cwxS1ZpSu2hAMRcqbt · head d7dd7d7 · 2026-11-14T17:19Z

review#1 (E1) takes this head.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d7dd7d7b1333eae9a128ba36a960735ebb9f595e
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts + packages/metadata-protocol/src/protocol-diff-delete-load.ts, packages/metadata-protocol/src/protocol-stored-expression-fill.ts, packages/metadata-protocol/src/protocol.ts, packages/objectql/src/stored-expression-backfill.test.ts, packages/spec/src/api/protocol.test.ts, packages/spec/authorable-surface/api.json, content/docs/references/api/protocol.mdx, .changeset/32958-delete-extension-fill-stored-expressions.md (read: packages/objectql/src/stored-expression-backfill.ts captureStoredExpressions / changedSince)

Face: schema (a packages/spec/src/** hit). Diff read against merge base 199e3624. PR is Part of #32958; the card's second item (prose in protocol-registry-hydration.ts) is deliberately left out.

① Derived judgments

  • The delete door now runs the one fill path (captureStoredExpressionChange → fillStoredExpressionChange) the save, publish, rollback and revert doors run. The capture is taken only for targetState === 'active', before restoreArtifactRegistryView. The fill runs after the audit row, projector and mutation event, before the answer, which is the save door's order. A draft discard captures nothing.
  • filledObjectOf keys object_extension by the object it extends, so name is the right object. A deleted object with nothing beneath it serves no fields afterwards: changedSince(before, undefined) is empty, so no row is read (checked in stored-expression-backfill.ts). This matches the new comment.
  • advisories is spread only when non-empty, so a clean delete's bytes are unchanged. The fill never throws into the delete (fillStoredExpressionChange reports instead).
  • The pins cover both save orders (object then extension, extension then object) with a red-on-main premise, a non-stored extension (no rows written, no key), a draft discard, and a schema round-trip of the produced answer through DeleteMetaItemResponseSchema.parse. The spec test flips the old "declares neither" pin to "does not declare version" plus a positive advisories pin.
  • CI on this head was still running at review time (16 success, 9 skipped, 20 in progress, none failed). Landing needs all checks green on this head, which this record does not replace.

② Semver level

The changeset declares Clause-②: yes (widening) (a new optional response key) and Metadata-protocol: added. @objectstack/spec is minor and @objectstack/metadata-protocol is patch. That meets the gate's rule (a declared kind grades at least one moved package minor) and AGENTS.md Post-Task Checklist step 3. No ADR-0087 marker is owed for a widening. The authorable-surface baseline and the reference doc are regenerated in the same PR.

③ Boundary flags

  • No governed path. The PR is authored by objectstack-fleet[bot] on the relay.
  • N1 (nit): filledObjectOf also answers for type object, so deleting the stored row of an artifact-backed object now fills too, when the restored artifact definition changes a stored expression. That is correct and consistent with the save door, but the changeset and the schema description name only the object_extension case, and there is no pin for it. One sentence, and optionally one it, would cover it.
  • N2 (nit): the DeleteMetaItemResponseSchema docblock says advisories comes "on the repository path's row-deleted return alone". Accurate for this head. If another delete branch ever gains a fill, that sentence needs to move with it.

④ Design judgment

protocol.zod.ts: the new optional advisories reuses RuntimeAuthoringIssueSchema, the save answer's element shape, with the same omitted-when-empty discipline. A client that ignores the key behaves as before, and z.object strips nothing it did not already strip. The rewritten docblock keeps the declared-equals-returned discipline. It still refuses version with its reason, and it says plainly that advisories carries no #4463 gate finding, only the fill. The first consumer (the delete door) emits exactly that shape, pinned through parse. The implementing files agree with the contract. No must-fix or should-fix; N1–N2 are nits. This is the final round.

Implemented-by: claude/area-E1-32958
Reviewed-by: session_016WV4cwxS1ZpSu2hAMRcqbt

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cy1y7UNVbT2QAdmgdn

ACCEPT · area E1 · 2026-11-14T17:48Z

  • Part of metadata-protocol: deleting an own-package object_extension that re-declared a stored expression leaves stored values stale #32958 (the card's second item, the objectStorageSyncRefusedNotice prose, follows in a separate E1 PR that closes the card).
  • Reviewer: review#1, contract-review record #issuecomment-6881370237, VERDICT: PASS on head d7dd7d7b1333eae9a128ba36a960735ebb9f595e (no must-fix / should-fix).
  • Governed: check-governed-merges --pr 33350 — NOT governed, 228 changed lines.
  • Checks on this head: 58 success, 10 skipped, none failed. Mergeable clean; main has not touched any file of this PR, so no main merge (pilot rule).
  • Nits N1–N2 go to that follow-up E1 PR.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 17:49
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit 61ed314 Nov 14, 2026
69 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/area-E1-32958 branch November 14, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants